﻿using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;

namespace XEnamel
{
	public class XmlToExpression : IScopeManager
	{
		#region Constants

		private const string SYMBOL_FORMAT = @"[A-Za-z_][A-Za-z_0-9\.]*";
		private const string NAME_PROPERTY = "Name";
		private const string TYPE_PROPERTY = "Type";
		private static readonly Type DEFAULT_TYPE = typeof(object);

		#endregion

		#region Variables

		private Dictionary<string, Func<XElement, Expression>> _nodeHandlers;
		private Stack<Dictionary<string, ParameterExpression>> _symbolScope;
		private Stack<Dictionary<string, LabelTarget>> _labelScope;

		#endregion

		#region Constructors

		public XmlToExpression()
		{
			_symbolScope = new Stack<Dictionary<string, ParameterExpression>>();
			_labelScope = new Stack<Dictionary<string, LabelTarget>>();

			_nodeHandlers = new Dictionary<string, Func<XElement, Expression>>()
			{
				{ "Object", VisitObject },
				{ "Lambda", VisitLambda },
				{ "Parameter", VisitParameter },
				{ "Block", VisitBlock },
				{ "Constant", VisitConstant },
				{ "Loop", VisitLoop },
				{ "Conditional", VisitConditional },
				{ "If", VisitConditional },
				{ "IfThen", VisitConditional },
				{ "IfThenElse", VisitConditional },
				{ "Assign", VisitBinary },
				{ "GreaterThan", VisitBinary },
				{ "LessThan", VisitBinary },
				{ "Or", VisitBinary },
				{ "Equal", VisitBinary },
				{ "Add", VisitBinary },
				{ "Subtract", VisitBinary },
				{ "Multiply", VisitBinary },
				{ "Divide", VisitBinary },
				{ "AddAssign", VisitBinary },
				{ "SubtractAssign", VisitBinary },
				{ "MultiplyAssign", VisitBinary },
				{ "DivideAssign", VisitBinary },
				{ "PostDecrementAssign", VisitUnary },
				{ "PostIncrementAssign", VisitUnary },
				{ "Throw", VisitUnary },
				{ "New", VisitNew },
				{ "Goto", VisitGoto },
				{ "MemberAccess", VisitMemberAccess },
				{ "Property", VisitMemberAccess },
				{ "Call", VisitCall },
				{ "Dynamic", VisitDynamic },
				{ "Symbol", VisitSymbol },
				{ "Return", VisitReturn },
				{ "Label", VisitLabel }
			};
		}

		#endregion

		#region Methods

		public Expression Translate(XElement node)
		{
			if (_nodeHandlers.ContainsKey(node.Name.LocalName))
			{
				return _nodeHandlers[node.Name.LocalName](node);
			}
			throw new NotImplementedException();
		}

		#region Scope Management

		public bool IsSymbolNameValid(string name)
		{
			return Regex.Match(name, SYMBOL_FORMAT).Success;
		}

		public void EnterScope()
		{
			_symbolScope.Push(new Dictionary<string, ParameterExpression>());
			_labelScope.Push(new Dictionary<string, LabelTarget>());
		}

		public void LeaveScope()
		{
			_symbolScope.Pop();
			_labelScope.Pop();
		}

		public Expression FindSymbol(string name)
		{
			foreach (var map in _symbolScope)
			{
				if (map.ContainsKey(name))
				{
					return map[name];
				}
			}
			return null;
		}

		public Expression CreateSymbol(string name, Type type = null)
		{
			if (!Regex.Match(name, SYMBOL_FORMAT).Success)
			{
				throw new ArgumentException(string.Format("Symbol name is invalid, expected: {0}", SYMBOL_FORMAT), "name");
			}
			_symbolScope.Peek()[name] = Expression.Parameter(type ?? typeof(object), name);
			return _symbolScope.Peek()[name];
		}

		public LabelTarget VisitLabelTarget(string name, Type type = null)
		{
			return FindLabel(name) ?? CreateLabel(name, type);
		}

		public LabelTarget FindLabel(string name)
		{
			return _labelScope
				.Where(map => map.ContainsKey(name))
				.Select(map => map[name]).FirstOrDefault();
		}

		public LabelTarget CreateLabel(string name, Type type = null)
		{
			_labelScope.Peek()[name] = Expression.Label(type ?? typeof(object), name);
			return _labelScope.Peek()[name];
		}

		#endregion

		#region Node Visitors

		/// <summary>
		/// The "Object" node is a Lambda with a self parameter inserted at the beginning of the parameter list.
		/// </summary>
		/// <param name="node"></param>
		/// <returns></returns>
		private LambdaExpression VisitObject(XElement node)
		{
			EnterScope();
			var parameters = (node.PropertyType("Parameters") != XmlNodeType.None) ? VisitExpressionList(node, "Parameters").Cast<ParameterExpression>().ToList() : new List<ParameterExpression>();
			parameters.Insert(0, (ParameterExpression)CreateSymbol("self", typeof(DynamicMethod)));
			var expr = Expression.Lambda(
				VisitNodeExpressionProperty(node, "Body"),
				parameters);
			LeaveScope();
			return expr;
		}

		/// <remarks>
		/// The parameters need to be retrieved BEFORE visiting the Lambda Body.
		/// If you don't do this the parameters will not be assigned to the correct scope.
		/// This means that you need to retrieve the parameter list BEFORE defining the Lambda expression,
		/// and casting it to a List to make sure the scope is actually populated.
		/// 
		/// Don't go fooling around with this; you will only confuse yourself!
		/// </remarks>
		private LambdaExpression VisitLambda(XElement node)
		{
			EnterScope();
			var parameters = (node.PropertyType("Parameters") != XmlNodeType.None) ? VisitExpressionList(node, "Parameters").Cast<ParameterExpression>().ToList() : null;
			var expr = Expression.Lambda(
				VisitNodeExpressionProperty(node, "Body"),
				parameters);
			LeaveScope();
			return expr;
		}

		private Expression VisitParameter(XElement node)
		{
			var name = GetExpressionName(node);
			return FindSymbol(name) ?? CreateSymbol(name, GetExpressionType(node));
		}

		private BlockExpression VisitBlock(XElement node)
		{
			EnterScope();
			var variables = (node.PropertyType("Variables") != XmlNodeType.None) ? VisitExpressionList(node, "Variables").Cast<ParameterExpression>().ToList() : null;
			var expr = Expression.Block(
				variables,
				VisitExpressionList(node, "Expressions"));
			LeaveScope();
			return expr;
		}

		private Expression VisitBinary(XElement node)
		{
			var expressionType = (ExpressionType)Enum.Parse(typeof(ExpressionType), node.Name.LocalName, true);
			var left = VisitNodeExpressionProperty(node, "Left");
			var right = VisitNodeExpressionProperty(node, "Right");
			try
			{
				return Expression.MakeBinary(expressionType, left, right);
			}
			catch (ArgumentException)
			{
				return Expression.MakeBinary(expressionType, left, right.ChangeType(left.Type).Convert(left.Type));
			}
			catch (InvalidOperationException)
			{
				// Didn't work for a standard expression type, so try a dynamic one:
				return Expression.Dynamic(new SimpleBinaryOperationBinder(expressionType), GetExpressionType(node), left, right);
			}
		}

		private ConstantExpression VisitConstant(XElement node)
		{
			var type = GetExpressionType(node);
			return Expression.Constant(Convert.ChangeType(node.Attribute("Value").Value, type), type);
		}

		private LoopExpression VisitLoop(XElement node)
		{
			EnterScope();
			var label = VisitNodeLabelProperty(node, "BreakLabel");
			var expr = Expression.Loop(VisitNodeExpressionProperty(node, "Body"), label);
			LeaveScope();
			return expr;
		}

		private LabelTarget VisitLabelTarget(XElement node)
		{
			return VisitLabelTarget(GetExpressionName(node), GetExpressionType(node));
		}

		private ConditionalExpression VisitConditional(XElement node)
		{
			var test = VisitNodeExpressionProperty(node, "Test");
			if (test.Type != typeof(bool))
			{
				test = test.ChangeType<bool>().Convert<bool>();
			}
			if (node.HasProperty("IfFalse") || node.HasProperty("False"))
			{
				return Expression.Condition(
					test,
					(VisitNodeExpressionProperty(node, "IfTrue") ?? VisitNodeExpressionProperty(node, "True")).Convert<object>(),
					(VisitNodeExpressionProperty(node, "IfFalse") ?? VisitNodeExpressionProperty(node, "False")).Convert<object>(),
					GetExpressionType(node));
			}
			else
			{
				var ifTrue = VisitNodeExpressionProperty(node, "IfTrue") ?? VisitNodeExpressionProperty(node, "True");
				return Expression.Condition(test, ifTrue, Expression.Constant(ifTrue.Type.IsValueType ? Activator.CreateInstance(ifTrue.Type) : null, ifTrue.Type), ifTrue.Type);
			}
		}

		private UnaryExpression VisitUnary(XElement node)
		{
			var expressionType = (ExpressionType)Enum.Parse(typeof(ExpressionType), node.Name.LocalName, true);
			var operand = VisitNodeExpressionProperty(node, "Operand");
			return Expression.MakeUnary(expressionType, operand, operand.Type);
		}

		private NewExpression VisitNew(XElement node)
		{
			if (node.HasProperty("Arguments"))
			{
				var arguments = VisitExpressionList(node, "Arguments");
				var argumentTypes = arguments.Select(x => x.Type).ToArray();
				return Expression.New(GetExpressionType(node).GetConstructor(argumentTypes), arguments);
			}
			return Expression.New(GetExpressionType(node));
		}

		private GotoExpression VisitGoto(XElement node)
		{
			var target = VisitNodeLabelProperty(node, "Target");
			var value = VisitNodeExpressionProperty(node, "Value");
			var type = GetExpressionType(node) ?? ((value == null) ? typeof(void) : value.Type);
			return Expression.Goto(target, value, type);
		}

		private MemberExpression VisitMemberAccess(XElement node)
		{
			return Expression.Property(VisitNodeExpressionProperty(node, "Source"), GetExpressionName(node));
		}

		private Expression VisitCall(XElement node)
		{
			var methodDesc = new MethodDescriptor() { DeclaringType = null, MethodName = node.Attribute("Name").Value };
			var arguments = VisitExpressionList(node, "Arguments");
			var argumentTypes = arguments.Select(x => x.Type).ToArray();
			var instance = VisitNodeExpressionProperty(node, "Object");

			if (instance != null)
			{
				methodDesc.DeclaringType = instance.Type;
			}
			else
			{
				// Parse a static method call.

				if (node.Attribute("DeclaringType") != null)
				{
					methodDesc.DeclaringType = TypeHelper.FindType(node.Attribute("DeclaringType").Value);
				}
				else
				{
					methodDesc = TypeHelper.ParseFullyQualifiedMethodName(methodDesc.MethodName);
				}
			}

			if (methodDesc.DeclaringType != null)
			{
				return Expression.Call(instance, methodDesc.GetMethodInfo(argumentTypes), methodDesc.ConvertArguments(arguments));
			}
			else
			{
				return ExpressionEx.DynamicInvoke(new Evaluator(this).Evaluate(methodDesc.MethodName), arguments.ToArray());
			}
		}

		private DynamicExpression VisitDynamic(XElement node)
		{
			var operation = (ExpressionType)Enum.Parse(typeof(ExpressionType), node.Attribute("Operation").Value);
			var arguments = VisitExpressionList(node, "Arguments");
			var binder = (operation == ExpressionType.Invoke)
				? (CallSiteBinder)new SimpleInvokeBinder(new CallInfo(arguments.Count()))
				: new SimpleBinaryOperationBinder(operation);
			return Expression.Dynamic(binder, GetExpressionType(node), arguments);
		}

		private Expression VisitSymbol(XElement node)
		{
			var scope = VisitNodeExpressionProperty(node, "Scope");
			var symbolName = node.Attribute("Name").Value;
			return ExpressionEx.SymbolExpression(scope, symbolName);
		}

		private Expression VisitReturn(XElement node)
		{
			return Expression.Return(VisitNodeLabelProperty(node, "Target"), VisitNodeExpressionProperty(node, "Value"), GetExpressionType(node));
		}

		private Expression VisitLabel(XElement node)
		{
			return Expression.Label(VisitNodeLabelProperty(node, "Target"), VisitNodeExpressionProperty(node, "Default"));
		}

		private IEnumerable<Expression> VisitExpressionList(XElement node, string propertyName)
		{
			if (node.Attribute(propertyName) != null)
			{
				return new Evaluator(this).EvaluateList(node.Attribute(propertyName).Value);
			}
			else
			{
				return node.Elements(string.Format("{0}.{1}", node.Name.LocalName, propertyName)).Elements().Select(Translate);
			}
		}

		private Expression VisitNodeExpressionProperty(XElement node, string propertyName)
		{
			return VisitExpressionList(node, propertyName).DefaultIfEmpty(null).SingleOrDefault();
		}

		private LabelTarget VisitNodeLabelProperty(XElement node, string propertyName)
		{
			switch (node.PropertyType(propertyName))
			{
				case XmlNodeType.Element:
					return VisitLabelTarget(node.Element(string.Format("{0}.{1}", node.Name, propertyName)).Elements().Single());
				case XmlNodeType.Attribute:
					return new Evaluator(this).EvaluateLabel(node.Attribute(propertyName).Value);
				default:
					return null;
			}
		}

		#endregion

		#region Helper Methods

		private Type GetExpressionType(XElement node)
		{
			return (node.PropertyType(TYPE_PROPERTY) != XmlNodeType.None)
				? TypeHelper.FindType(node.Attribute(TYPE_PROPERTY).Value)
				: typeof(object);
		}

		private string GetExpressionName(XElement node)
		{
			return node.Attribute(NAME_PROPERTY).Value;
		}

		#endregion

		#endregion
	}
}